﻿/* Copyright (c) 2010 EPiServer AB
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
 * of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using EPiServer.Web;
using EPiServer.Web.Hosting;
using System.IO;
using System.Web.Hosting;
using System.Globalization;
using System.Configuration;
using System.Text.RegularExpressions;
using EPiServer.Security;
using EPiServer.Core;
using System.Diagnostics;
using log4net;

namespace EPiServer.CdnSupport
{
    /// <summary>
    /// Http module to register CDN support
    /// </summary>
    public class CdnModule : IHttpModule
    {
        private static bool     _hasInit    = false;
        private static object   _initLock   = new object();
        private static Regex    _validateUrl;
        private static MicroCache<Uri> _cache;

        private const string ClientPreferedStatusToken = "CdnModule-ClientPreferedStatus";
        private const string CdnInstance = "CdnModule-Instance";
        private const string IsTimestampRequest = "CdnModule-IsTimestampRequest";
        private const string IsRequestAuthorizedToDebug = "CdnModule-IsAuthorizedToDebug";

        private static readonly ILog _log = LogManager.GetLogger(typeof(CdnModule));

        /// <summary>
        /// Initialize the Http Module
        /// </summary>
        /// <param name="context"></param>
        public void Init(HttpApplication context)
        {
            context.BeginRequest += new EventHandler(OnBeginRequest);

            if (_hasInit)
                return;

            lock (_initLock)
            {
                if (_hasInit)
                    return;

                _cache = new MicroCache<Uri>(true,TimeSpan.FromMinutes(10),TimeSpan.FromMinutes(3),10000,0.75);
                _validateUrl = new Regex("^/cdn-[a-z0-9]{15}/.+", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);

                InitializationModule.FirstBeginRequest += new EventHandler(InitializationModule_FirstBeginRequest);

                _hasInit = true;
            }
        }

        /// <summary>
        /// Our first time initialization based on CMS standards
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void InitializationModule_FirstBeginRequest(object sender, EventArgs e)
        {
            EPiServer.PageBase.PageSetup += new PageSetupEventHandler(OnPageBaseSetup);
            UnifiedFile.UnifiedFileChanged += new UnifiedFileStreamEventHandler(OnUnifiedFileChanged);
            HtmlRewriteToExternal.HtmlRewriteInit += new EventHandler<HtmlRewriteEventArgs>(HtmlRewriteToExternal_HtmlRewriteInit);
        }

        /// <summary>
        /// Hook our event for the url rewriting engine
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void HtmlRewriteToExternal_HtmlRewriteInit(object sender, HtmlRewriteEventArgs e)
        {
            ((HtmlRewriteToExternal)sender).HtmlRewroteUrl += new EventHandler<UrlRewriteEventArgs>(OnConvertedToExternal);
        }

        /// <summary>
        /// Handle expiration of changed files from the cache
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void OnUnifiedFileChanged(UnifiedFileStream sender, UnifiedVirtualPathEventArgs e)
        {
            if (!String.IsNullOrEmpty(e.NewVirtualPath))
            {
                _log.DebugFormat("Got OnUnifiedFileChanged for '{0}'", e.NewVirtualPath);
                if (!_cache.Remove(HttpUtility.UrlPathEncode(e.NewVirtualPath)))
                {
                    _cache.Remove(e.NewVirtualPath);
                }
            }
        }

        /// <summary>
        /// Called whenever a new PageBase instance is being initialized
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private static void OnPageBaseSetup(PageBase sender, PageSetupEventArgs e)
        {
            sender.PreRenderComplete += new EventHandler(OnPreRenderComplete);
        }

        /// <summary>
        /// Called whenever a the PreRenderComplete event is called
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        /// <remarks>This is where we actually fire up the Cdn module to prepare for its work</remarks>
        private static void OnPreRenderComplete(object sender, EventArgs e)
        {
            PageBase p = sender as PageBase;
            HttpContext c = HttpContext.Current;

            if (p == null || c == null || p.CurrentPage == null)
                return;

            if (c.Items.Contains(CdnInstance))
                return;

            CdnSupportSection config = CdnSupportSection.GetCurrent();

            if (p.CurrentPage.PageLink.WorkID == 0 && config != null && config.Enabled)
            {
                _log.DebugFormat("Creating Cdn instance on page {0}",p.CurrentPage.PageLink);
                c.Items[CdnInstance] = new Cdn(config.Url, config.Timestamp, config.Extensions, p.CurrentPage.ACL.QueryAccess(PrincipalInfo.AnonymousPrincipal, AccessLevel.Read) == AccessLevel.Read, VirtualPathHandler.PageDirectoryRootVirtualPath);

            }

            if (IsAuthorizedToDebug(c))
            {
                OnPreRenderDebuggingSupport(c);
            }
        }

        /// <summary>
        /// Handles the debugging part of PreRender
        /// </summary>
        /// <param name="c"></param>
        private static void OnPreRenderDebuggingSupport(HttpContext c)
        {
            if (c.Request.QueryString["cdn"] != null && c.Request.QueryString["cdn"].Length > 0)
            {
                string setting = c.Request.QueryString["cdn"] == "off" ? "off" : "on";
                c.Items[ClientPreferedStatusToken] = setting;
                c.Response.Cookies[ClientPreferedStatusToken].Value = setting;
                _log.DebugFormat("Setting CDN status to (debug): {0}", setting);
            }

            if (c.Request.QueryString["cdn-microcache"] == "debug")
            {
                c.Response.Write(String.Format("MicroCache Stats: Count: {0} CacheHits:{1} CacheMisses: {2} CacheBusy: {3} CacheAdded: {4} CacheRemoved: {5}", _cache.Count, _cache.CacheHits, _cache.CacheMisses, _cache.CacheBusy, _cache.CacheAdded, _cache.CacheRemoved));
            }
        }

        /// <summary>
        /// Check if the current user is authorized to do any debugging actions
        /// </summary>
        /// <param name="c"></param>
        /// <returns></returns>
        private static bool IsAuthorizedToDebug(HttpContext c)
        {
            object cachedSuccess = c.Items[IsRequestAuthorizedToDebug];

            if (cachedSuccess != null)
                return (bool)cachedSuccess;

            CdnSupportSection config = CdnSupportSection.GetCurrent();
            bool success = false;

            if (config.DebuggerRole=="Anonymous")
            {
                success = true;
            }
            else if(!c.Request.IsAuthenticated)
            {
                success = false;
            }
            else
            {
                success = c.User.IsInRole(config.DebuggerRole);
            }

            c.Items.Add(IsRequestAuthorizedToDebug, success);
            return success;
        }

        public void Dispose()
        {
        }

        /// <summary>
        /// Helper method to manually remap a path with CDN Support
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        /// <remarks>This method can be used by external code that need to do a manual rewrite</remarks>
        public static string MapPath(string path)
        {
            UrlRewriteEventArgs e = new UrlRewriteEventArgs(new UrlBuilder(path));
            OnConvertedToExternal(null, e);
            if (e.IsModified)
                return e.Url.Uri.ToString();
            else
                return path;
        }

        /// <summary>
        /// Used to remove the timestamp on incoming requests
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private static void OnBeginRequest(object sender, EventArgs e)
        {
            HttpContext c = HttpContext.Current;

            if (_validateUrl.IsMatch(c.Request.Path))
            {
                string newPath = HttpUtility.UrlDecode(c.Request.Path).Substring(20);
                c.Items[IsTimestampRequest] = true;

                _log.DebugFormat("Rewriting {0} to {1}",c.Request.Path,newPath);
                c.RewritePath(newPath, String.Empty, String.Empty, true);
            }
        }

        /// <summary>
        /// Check if this request comes from a CDN (both has a timestamp and is not authenticated)
        /// </summary>
        /// <param name="ctx"></param>
        /// <returns></returns>
        public static bool IsRequestComingFromCdn(HttpContext ctx)
        {
            return !ctx.Request.IsAuthenticated && ctx.Items[IsTimestampRequest] != null && (bool)ctx.Items[IsTimestampRequest];
        }

        /// <summary>
        /// Called whenever the url rewriter finds something to rewrite
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private static void OnConvertedToExternal(object sender, UrlRewriteEventArgs e)
        {
            HttpContext c = HttpContext.Current;
            if (c == null)
                return;

            Cdn cdn = c.Items[CdnInstance] as Cdn;
            if (cdn == null)
            {
                return;
            }

            if (IsAuthorizedToDebug(c))
            {
                if (!OnConvertedToExternalDebug(c))
                {
                    return;
                }
            }
            
            if (!cdn.CanRewriteToCdn(e))
            {
                if (_log.IsDebugEnabled)
                {
                    _log.DebugFormat("Cannot rewrite URL to CDN: {0}", e.Url.Path);
                }
                return;
            }

            bool cacheBusy;
            string originalPath = e.Url.Path;
            Uri newUri = _cache.Get(originalPath, out cacheBusy);
            if (newUri == null)
            {
                if (_log.IsDebugEnabled)
                {
                    _log.DebugFormat("Cache hit for: {0}", originalPath);
                }
                e.Url.Uri = cdn.RewriteUrl(e.Url, delegate(string path) { return c.Server.MapPath(path); });
                if (!cacheBusy)
                {
                    _cache.Add(originalPath, e.Url.Uri);
                }
            }
            else
            {
                if (_log.IsDebugEnabled)
                {
                    _log.DebugFormat("Cache miss for: {0}", originalPath);
                }
                e.Url.Uri = newUri;
            }

            e.Cancel = false;
            e.IsModified = true;
        }

        /// <summary>
        /// Helper method to support debugging actions
        /// </summary>
        /// <param name="c"></param>
        /// <returns></returns>
        private static bool OnConvertedToExternalDebug(HttpContext c)
        {
            if (c.Items[ClientPreferedStatusToken] != null)
            {
                string setting = c.Items[ClientPreferedStatusToken] as string;
                if (setting == "off")
                {
                    _log.Debug("User disabled CDN(debug)");
                    return false;
                }
            }
            else if (c.Request.Cookies[ClientPreferedStatusToken] != null)
            {
                string setting = c.Request.Cookies[ClientPreferedStatusToken].Value;
                if (setting == "off")
                {
                    _log.Debug("User disabled CDN(debug)");
                    return false;
                }
            }
            return true;
        }
    }
}
